#!/usr/bin/env perl

=head1 NAME

QueryProcessor

=head1 SYNOPSIS

use QueryProcessor;
use Scorer;

my $scorer = Scorer->new(maxRMSD=>5, scoreRangeMax=>100)
my $queryProcessor = QueryProcessor->new(pathCofm=>"./", scorer=>$scorer);

my $loop=1;
while($loop){
	my($anotherQueryNeeded, @results) = $queryProcessor->query("1e79", "2xnd", 3, inf, 100);
	$loop = $anotherQueryNeeded;
}

=head1 DESCRIPTION

Query Processor serves as a control structure that enables comparisons of two protein complexes to be processed
iteratively obtaining intermediate results each time 'query' is executed. This allows for intermediate results to
be used directly instead of having to wait for the completion of the comparison process. Two complexes to be searched
for shared subcomplexes need to be specified. In cases where these complexes comprise many domains, it may be necessary
to apply an approximative heuristic. The threshold for this heuristic to bes used can be specfied. Separate from that, 
the number of operations in one query can be limited, such that each query takes almost the same time. The next query 
then continues from the state where the last query was ended without loss of information. Please refer to the description 
of the 'query' function for more details.

=head1 REQUIRES

sbglib, PDL

=head1 AUTHOR

Philipp Harth <phil.harth@gmail.com>

=cut

use PDL;
use Set::Scalar;
use Log::Log4perl qw(:easy);

use Scorer;
use SubcomplexFinder;
use SubcomplexMerger;
use Util;

use SBG::DomainIO::cofm;

package QueryProcessor;
use Moose;

has 'pathCofm' => (is=> 'ro', default=>"./");
has 'scorer' => (is => 'ro', isa=> 'Scorer');

#initialized in BUILD
has 'subcomplexFinder' => (is=>'rw');
has 'subcomplexMerger' => (is=>'rw');

#state variable:
#  0: find initial subcomplexes
#  1: merge subcomplexes 
has 'state' => (is => 'rw', default=>0);

#is set to 1 once approximationThreshold has been applied
has 'approximative' => (is=>'rw', default=>0);

#counts queries between 'init' and 'reset'
has 'counter' => (is => 'rw' , default=>0);

# subcomplexes that are currently merged have 'mergeLevel' domains
has 'mergeLevel' =>(is =>'rw',default=>1);

#complete results from last stage and input for current merging stage
has 'identifiedSubcomplexes' => (is=>'rw', isa=>'ArrayRef[Subcomplex]', default=> sub{[]});
#intermediate results from current merging stage
has 'intermediateResults' => (is=>'rw', isa=>'ArrayRef[Subcomplex]', default=> sub{[]});

#initializes the subcomplex finder and the subcomplex merger modules
sub BUILD{
	my ($self) = @_;
	$self->subcomplexFinder(SubcomplexFinder->new(scorer=>$self->scorer));
	$self->subcomplexMerger(SubcomplexMerger->new(scorer=>$self->scorer));
}

=head2 query

 Function: resets Query Processor
 Example : 
 Returns : 
 Args    : 
 
Clears all intermediate results and resets internal state variables. Call before any new protein complex pair
is searched for shared subcomplexes with 'query'.
 
=cut
sub reset{
	my ($self) = @_;
	$self->state(0);
	$self->approximative(0);
	$self->counter(0);
	$self->mergeLevel(1);
	$self->identifiedSubcomplexes(());
	$self->intermediateResults(());
	$self->subcomplexMerger->reset();
}


=head2 query

 Function: executes a query 
 Example : 
 Returns : 
 Args    : PDB-ID of query, PDB-ID of candidate, k, mergeOps, approximationThreshold
 
The first two parameters specifiy the complexes that are searched for shared subcomplexes. If the last three parameters are
all set to inf, 'query' returns all shared subcomplexes that it finds, may take arbitrary many operations, and does not apply
an approximative heuristic. If the 'k' parameter is other than inf, only the k best subcomplexes (as by RMSD) are returned. 
Setting the 'k' parameter has no impact on the number of operations 'query' takes and it does not lead to approximative results.
It only limits the number of results that are returned. Setting 'mergeOps' other than inf essentially limits the number of 
operations during one call of 'query'. It does not lead to approximative results, because the state is maintained till the next call 
of 'query'. This allows for intermediate results to be obtained and long queries to be partitioned into smaller queries without loss
of information. If 'approximationThreshold' is set other than inf, 'query' may use an approximative heuristic. In consequence, not all
shared subcomplexes may be found. However, the subcomplexes that are found are in no way impacted by the heuristic (i.e., they are
exactly the same as in the case no heuristic is applied). In other words, using the heuristic impacts the number of results, but not the
quality of the results. Specifically, 'approximationThreshold' limits the number of subcomplexes that are passed to the next stage. The
first 'approximationThreshold' are passed on, the rest is abandoned.
 
=cut
sub query{
	my ($self, $query, $candidate, $k, $mergeOps, $approximationThreshold)= @_;
	
	my $logger = Log::Log4perl::get_logger();
	$self->counter($self->counter + 1); 
						
	if($self->state == 0){ #determine initial subcomplexes from domains if state is 0
		
		#load domains of query complex
		my @queryDomains = Util::getDomains($query, $self->pathCofm);
	
		#load domains of candidate complex
		my @candidateDomains = Util::getDomains($candidate, $self->pathCofm);	
		
		$logger->info("query call ",$self->counter,", merge level ",$self->mergeLevel,": ",scalar(@queryDomains)," + ",
					   scalar(@candidateDomains)," domains in");
		
		#find initial subcomplexes
		my @subcomplexes = $self->subcomplexFinder->pairwise(\@queryDomains, \@candidateDomains);
		
		$logger->info("query call ",$self->counter,", merge level ",$self->mergeLevel,": ",scalar(@subcomplexes)," subcomplexes out");
		
		#apply approximation threshold
		if(scalar(@subcomplexes) > $approximationThreshold){
			@subcomplexes = @subcomplexes[0..$approximationThreshold-1];
			$self->approximative(1);
			$logger->info("query call ",$self->counter,", merge level ",$self->mergeLevel,": limited to ",scalar(@subcomplexes));
		}
		
		#append to results that will be input to the next stage
		@{$self->identifiedSubcomplexes} = ();
		push @{$self->identifiedSubcomplexes}, @subcomplexes; 
		
		$self->state(1);
		$self->mergeLevel($self->mergeLevel+1);
		
		#return best k results, another query is only required if there are more than 2 results that could be merged in the next stage
		my $anotherQueryRequired = 1;
		if(scalar(@subcomplexes) >= 2){
				$anotherQueryRequired = 1;
			} else {
				$anotherQueryRequired = 0;
			};	
		return ($anotherQueryRequired, getBestK(\@subcomplexes, $k));

	
	}else{ #merge existing subcomplexes if state is 1


		$logger->info("query call ",$self->counter,", merge level ",$self->mergeLevel,": ",scalar(@{$self->identifiedSubcomplexes})," subcomplexes in"); 
		
		#merge subcomplexes
		#change $self->subcomplexMerger->mergePrefixMethod(...) to an alternative merging method, if implemented
		my ($finished,@subcomplexes) = $self->subcomplexMerger->mergePrefixMethod($self->identifiedSubcomplexes, $mergeOps);
		
		$logger->info("query call ",$self->counter,", merge level ",$self->mergeLevel,": ",scalar(@subcomplexes)," subcomplexes out"); 
		
		#apply approximation threshold
		if(scalar(@subcomplexes) > $approximationThreshold){
			@subcomplexes = @subcomplexes[0..$approximationThreshold-1];
			$self->approximative(1);
			$logger->info("query call ",$self->counter,", merge level ",$self->mergeLevel,": limited to ",scalar(@subcomplexes));
		}
		
		#append to intermediate results
		push @{$self->intermediateResults}, @subcomplexes;
		
		my $anotherQueryRequired = 1;
		
		#if this stage of merging is finished
		if($finished){
			$self->mergeLevel($self->mergeLevel+1);
			#replace current input to merger ($identifiedSubcomplexes) by accumulated output of merger ($intermediateResults)
			@{$self->identifiedSubcomplexes} = @{$self->intermediateResults};
			@{$self->intermediateResults} = ();
			#another query is only required if there are more than 2 results that could be merged in the next stage
			if(scalar(@{$self->identifiedSubcomplexes}) >= 2){
				$anotherQueryRequired = 1;
			} else {
				$anotherQueryRequired = 0;
			};	
		}
		
		#return best k results
		return ($anotherQueryRequired, getBestK(\@subcomplexes, $k));
	
	
	} #end of else block
}



=head2 getBestK

 Function: returns best k subcomplexes   
 Example : 
 Returns : list of subcomplexes
 Args    : reference to list of subcomplexes, k
 
 Sorts a list of subcompklexes based on RMSD and returns the k best subcomplexes. 
 If there are less than k subcomplexes, all subcomplexes are returned in sorted order.

=cut
sub getBestK{
	my ($subcomplexes, $k)=@_;
	@{$subcomplexes} = sort {$a->rmsd <=> $b->rmsd} @{$subcomplexes};
	if(scalar(@{$subcomplexes}) < $k){
			return @{$subcomplexes};
		} else {
			return @{$subcomplexes}[0..$k-1];		
		}
}

no Moose;
1;